Skip to content

Creation of a PG critic script. #1254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from

Conversation

pstaabp
Copy link
Member

@pstaabp pstaabp commented Jun 20, 2025

The pg-critic script analyzes a pgproblem (or list of them) for both good and bad features of a problem. Currently this include:

Positive features:

  • Uses PGML
  • Provides a solution
  • Provides a hint
  • Uses Scaffolds
  • Uses a custom checker
  • Uses a multianswer
  • Uses answer hints
  • Uses nicetables

Old and deprecated features

  • checking for deprecated macros
  • Use of BEGIN_TEXT/END_TEXT
  • Include the TEXT(beginproblem)
  • Include old tables (for example from unionTables.pl)
  • The use of num_cmp, str_cmp and fun_cmp in lieu of using MathObjects
  • Including Context()->TeXStrings
  • Calling loadMacros more than once.
  • Using the line $showPartialCorrectAnswers = 1 which is the default behavior and thus unnecessary.
  • Using methods from PGchoicemacros.pl
  • Including code or other text below the ENDDOCUMENT(); line indicating the end of the problem.

Currently the script can also score a problem (there is a rubric that is built-in to the script). This can be made more flexible.

Also, there are plans to include more features on both the positive and negative side.

Copy link
Member

@drgrice1 drgrice1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please set the executable bit on the pg-critic.pl script.

@pstaabp pstaabp force-pushed the pg-analysis branch 2 times, most recently from 8e69136 to 3756e61 Compare June 21, 2025 02:35
@pstaabp
Copy link
Member Author

pstaabp commented Jun 27, 2025

This improves the pg-critic script. Additional positive features are now scanned for including features in modern context, parsers and macros.

The script also now updates the help page for the script and provides a JSON output format which writes to a file--this might be helpful for scanning large numbers problems.

@duffee
Copy link
Contributor

duffee commented Jul 6, 2025

Nice work!

A suggestion for an optional critic policy is the amount of randomness available in the question. An anti-pattern I've seen by authors in a hurry (Contrib/BrockPhysics/College_Physics_Urone) is

$A1 = 63.5;
ANS(num_cmp("$A1"));

usually the result of values hardcoded in an image or just copied straight from an Answer Key. Other problems aside, this pgproblem has no randomness at all. It would be nice to know:

  • if there is randomness in the answers
  • estimate how many permutations available
  • range of answers (because I can call random, but not use it in a calculation)

It's more a pedagogic issue than authoring style, but the first two should be a Simple Matter of Programming ;)

@pstaabp
Copy link
Member Author

pstaabp commented Jul 7, 2025

Since this script just analyzes the code and doesn't run it, I think it will be difficult to test randomness of a problem by evaluating the problem for multiple seeds. We've talked about doing this at some point.

We could add a check for calling random or related methods within a problem. However, the following

$a = random(1,10);
$ans = Real(5);
BEGIN_PGML
[_]{$ans}
END_PGML

which would detect as using random, but won't be random.

@Alex-Jordan
Copy link
Contributor

Alex-Jordan commented Jul 7, 2025 via email

@dlglin
Copy link
Member

dlglin commented Jul 7, 2025

A simple first run would be to check for the word random. If it's not present then display a message like "it looks like this problem might not be randomized". This will obviously miss a bunch of problems that are not truly randomized, and might have some false positives as well, but it's easy to implement.

@pstaabp
Copy link
Member Author

pstaabp commented Jul 8, 2025

Just added a check for randomness by detecting functions of the form random(, list_random( or random_ which covers things like random_subset and random_coprime.

Copy link
Member

@drgrice1 drgrice1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that at this point, this and openwebwork/webwork2#2748 will need to be deferred until the next release. There are a lot of problems with the current code. The webwork2 pull request has many issues including things like invalid HTML. Both pull requests need code clean up.

The most serious issue though is with this pull request. We need to make sure that issues (both positive and negative) are correctly detected. It would be really bad if a positive aspect were detected as a negative aspect or vice versa, and that is happening at least to some extent.

Note that your code skips full line comments, but not code does not exclude comments at the end of a line. That means there could be incorrect results from these end of line comments.

Any problem that uses macros like parserPopUp.pl, parserRadioButtons.pl, or parserCheckboxList.pl with randomization of the choices but does not have any other random call in the problem are currently flagged as not having "randomness". That will definitely need to be fixed before this can be rolled out, since there are a lot of problems that use those macros. I think in general the randomness check will need to be done quite differently in order to work. I think that looking for the strings "random(" or "random_...(" or "random_list(" is not going to work. Those strings could be contained in an end of line comment and the current check will count it. Even if the end of line comment issue is fixed, these words could appear in an actual string in the code and the check will count that also.

Also, something is now wrong with the bin/pg-critic.pl script. All problems are returning a score of 0. This is because you must have changed from using "good" and "bad" in the PGProblemCritic.pm file to using "positive" and "negative", but didn't change the pg-critic-.pm script accordingly. Even with that fixed there are issues because of other changes in the module not in the script (particularly the addition of "randomness"). In general that brings into question the techniques used. The general data structure is not good.

In general, I don't think that this approach of parsing the code as text is going to work. A more versatile and reliable approach is needed.

@pstaabp pstaabp changed the base branch from PG-2.20 to develop July 14, 2025 19:59
@pstaabp
Copy link
Member Author

pstaabp commented Jul 14, 2025

Switched this to develop. Will continue to work on it.

drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 16, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
drgrice1 added a commit to drgrice1/pg that referenced this pull request Jul 22, 2025
This uses `Perl::Critic` and custom PG policies for `Perl::Critic` to
analyze the code.  The custom PG policies must be under the
`Perl::Critic::Policy` to be loaded by `Perl::Critic` (they give no
alternative for that).  That means they are in the
`lib/Perl/Critic/Policy` directory. Policies corresponding to everything
that was attempted to be detected in openwebwork#1254 have been implemented except
for `randomness`. `randomness` of a problem is far more complicated than
just checking if `random`, `list_random`, etc. are called.

Basically, the code of a problem is first translated (via the
`default_preprocess_code` method of the `WeBWorK::PG::Translator`
package), then converted to a `PPI::Document` (the underlying library
that `Perl::Critic` uses), and that is passed to `Perl::Critic`.

There are some utility methods provided in the
`WeBWorK::PG::Critic::Utils` package that can be used by the PG
policies. At this point those are `getDeprecatedMacros`,
`parsePGMLBlock`, and `parseTextBlock`.

The `getDeprecatedMacros` method just lists the macros in the
`macros/deprecated` directory.

The `parsePGMLBlock` method parses PGML contents, and actually uses
PGML::Parse for the parsing, and returns `PPI::Document` representations
of the content. At this point only command blocks are returned (perl
content of `[@ ... @]` blocks), but more can be added as needed by the
policies that are created.

The `parseTextBlock` method is similar but parses
`BEGIN_TEXT`/`END_TEXT` blocks (and the ilk) using a simplified
`ev_substring` approach.  At this point only the contents of `\{ ... \}`
blocks are returned, and other elements can be added later if needed.

Unfortunately, the `parsePGMLBlock` and `parseTextBlock` methods do not
give proper positioning within the code, so the line and column numbers
of the things in the return value will not be reliable.  The only policy
that uses these at this point is the
`Perl::Critic::Policy::PG::RequireImageAltAttribute` policy and that
just reports the violations as being inside the PGML or text block the
violations are found in.

Also, the original untranslated code is passed to the policies and can
be used if needed.  The `Perl::Critic::Policy::PG::ProhibitEnddocumentMatter`
is the only policy that uses this at this point.

Note that since this is just `Perl::Critic` this also reports violations
of the core `Perl::Critic` policies (at severity level 4).  However,
there are policies that clearly don't apply to PG problem code, and so
those are disabled.  For instance, obviously `use strict` and `use
warnings` can't be called in a problem, so the
`Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict` and
`Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings` policies
are disabled. The disabled policies start at line 57 of the
`WeBWorK::PG::Critic` package.  This may need tweaking as there may be
other policies that need to be disabled as well, but those are the
common violations that I have seen over the years using this for
problems that should not apply to problems (I have used a form of this
PG critic without the custom PG policies for some time now -- see
https://github.com/drgrice1/pg-language-server).

Also note that since this is just `Perl::Critic`, you can also use
`## no critic` annotations in the code to disable policy violations for
a specific line, the entire file, a specific policy on a specific line,
etc.  See https://metacpan.org/pod/Perl::Critic#BENDING-THE-RULES. For
example, if you have a problem that is in the works and are not ready to
add metadata, then add `## no critic (PG::RequireMetadata)` to the
beginning of the file, and you won't see the violations for having
missing metadata.  Note that the `bin/pg-critic.pl` script has a `-s`
or `--strict` option that ignores all `## no critic` annotations, and
forces all policies to be enforced.

The result is a reliable, versatile, and extendable approach for
critiquing problem code.

Since there was a desire to have a "problem score" and to reward good
behavior that has been implemented.  That means that not all
"violations" are bad.  Some of them are good.  The score is implemented
by setting the "explanation" of each violation as a hash which will have
the keys `score` and `explanation`.  The score will be positive if the
"violation" is good, and negative otherwise.  The `explanation` is of
course a string that would be the usual explanation.  This is a bit of a
hack since `Perl::Critic` expects the violation to be either a string or
a reference to an array of numbers (page numbers in the PBP book), but
the `explanation` method of the `Perl::Critic::Violation` object returns
the hash as is so this works to get the score from the policy.

Although, I am wondering if this "problem score" is really a good idea.
If we do start using this and make these scores public, will a low score
on a problem deter usage of the problem? It seems like this might
happen, and there are basic but quite good problems that are going to
get low scores simply because they don't need complicated macros and
code for there implementation. Will a high score really mean that a
problem is good anyway? What do we really want these scores for?  Some
sort of validation when our problems get high scores because they
utilize the things that happen to be encouraged at the time?  I am
thinking that this "problem score" idea really was NOT a good idea, and
should be removed.

If the score is removed, then there is also no point in the "positive
violations".  Those simply become a "pat on the back" for doing
something right which is really not needed (in fact that is all they
really are even with the score in my opinion).

So my proposal is to actually make this a proper critic that just shows
the things in a problem that need improvement, and remove the score and
the "positive violations". That is in my opinion what is really
important here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants